/**
 * Copyright 1996 self.com.au
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. (http://www.apache.org/licenses/LICENSE-2.0)
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for 
 * the specific language governing permissions and limitations under the License.
 *
 * <dhamma-message src="Atisha (11th century Tibetan Buddhist master)">
 * 	The greatest achievement is selflessness. The greatest worth is self-mastery.
 * 	The greatest quality is seeking to serve others. The greatest precept is continual awareness.
 * 	The greatest medicine is the emptiness of everything. The greatest action is not conforming with the worlds ways.
 * 	The greatest magic is transmuting the passions. The greatest generosity is non-attachment.
 * 	The greatest goodness is a peaceful mind. The greatest patience is humility.
 * 	The greatest effort is not concerned with results. The greatest meditation is a mind that lets go.
 * 	The greatest wisdom is seeing through appearances. 
 * </dhamma-message>
 *
 * @author Ashin Wimalajeewa
 *
 * Note, this is a very old runtime library that I have evolved since 1996. I rarely use it
 * now because the Apache Commons project has made it obsolete.
 */
package self.awt.painter;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;

import self.awt.ILineRenderInfo;
import self.math.EllipseEquation;
import self.math.LinearEquation;

// Non-synchronized utility
public class LinePainter {
//  private  LinePainter() {}
//  final static public LinePainter instance = new LinePainter();

  final static BasicStroke DASHED_STROKE = new BasicStroke(1.0f,
    BasicStroke.CAP_BUTT,
    BasicStroke.JOIN_MITER,
    5.0f, new float[] {5.0f}, 0.0f);

  private Graphics surface;
  private ILineRenderInfo line;
  private java.util.List points;
  private int max;
  private double[]  intersects = new double[4];
  private Point[]   headPoints = new Point[2];
  private Point     centre = new Point();
  private Rectangle displayBox = new Rectangle();

  private int[] xPoints = new int[4];
  private int[] yPoints = new int[4];

  private LinearEquation linear = new LinearEquation();
  private LinearEquation perp = new LinearEquation();
  private EllipseEquation circle = new EllipseEquation();

  public void paint( Graphics g, ILineRenderInfo l, boolean outlineOnly ) {
    line = l;
    points = l.getPoints();
    max = points.size();
    surface = g;
    drawLine();
    if ( !outlineOnly ) {
      drawHead( true );
      drawHead( false );
      drawNameAndHeaderDetails();
      drawMajorMinorDetails( true );
      drawMajorMinorDetails( false );
    }
  }

  private void drawLine() {
    surface.setColor( line.getColor() );
    Stroke curr = ((Graphics2D) surface).getStroke();
    BasicStroke strokeToUse = initBasicStroke( line );
    ((Graphics2D) surface).setStroke( strokeToUse );
    Point a = (Point) points.get( 0 );
    for ( int cntr = 1; cntr < max; cntr++ ) {
      Point b = (Point) points.get( cntr );
      surface.drawLine( a.x, a.y, b.x, b.y );
      a = b;
    }
    ((Graphics2D) surface).setStroke( curr );
  }

  static public BasicStroke initBasicStroke( ILineRenderInfo info ) {
    float[] dashedParam = (info.isDashed() ? new float[] {5.0f}: null);
    int capParam = (info.isDashed() ? BasicStroke.CAP_BUTT: BasicStroke.CAP_SQUARE);
    return new BasicStroke( info.getWidth(), capParam, BasicStroke.JOIN_MITER, 10f, dashedParam, 0.0f );
  }

  private void drawNameAndHeaderDetails() {
    String name = line.getLineName(), header = line.getLineHeader();
    if ( (name == null) && (header == null) )
      return;

    int diameterSize = line.getHeadDiameter();
    int radiusB = (int) ((20/12) * diameterSize);

    calculateCentre();
    linear.getPerpendicular( perp, centre.x, centre.y );
    circle.setCoefficients( radiusB, radiusB, centre.x, centre.y );
    circle.getIntersectingPoints( perp, intersects );
    self.math.geom.RectangleUtils.updateRectangleToTlbr( displayBox, (int)intersects[0], (int)intersects[1], (int)intersects[2], (int)intersects[3] );
    int y = displayBox.y;
    if ( name != null )
      surface.drawString( name, displayBox.x, y );
    if ( header != null ) {
      int height = (int)surface.getFontMetrics().getLineMetrics( "Z",surface ).getHeight();
      y -= ( name == null ? 0: height );
      surface.drawString( header, displayBox.x, y );
    }
  }

  private void drawHead( boolean sideA ) {
    int kind = sideA ? line.getSideAHead() : line.getSideBHead();
    if ( kind == ILineRenderInfo.NO_HEAD )
      return;
    headPoints[0] = sideA ? getFirstPoint() : getLastPoint();
    headPoints[1] = sideA ? getSecondPoint() : getSecondLastPoint();
    if ( (kind == ILineRenderInfo.WHITE_DIAMOND_HEAD) || (kind == ILineRenderInfo.BLACK_DIAMOND_HEAD) )
      drawDiamond( kind );
    else
      drawArrow( kind );
  }

  private void drawMajorMinorDetails( boolean sideA ) {
    int diameterSize = line.getHeadDiameter();

    String major = sideA ? line.getSideAMajor() : line.getSideBMajor();
    String minor = sideA ? line.getSideAMinor() : line.getSideBMinor();
    if ( (major == null) && (minor == null) )
      return;
    headPoints[0] = sideA ? getFirstPoint() : getLastPoint();
    headPoints[1] = sideA ? getSecondPoint() : getSecondLastPoint();

    if ( minor != null ) {
      int radiusB = (int) ((20/12) * diameterSize);
      getPointOnLineClosestToB( headPoints[0], headPoints[1], radiusB ); // 20
      boolean ignore = linear.m < 0 ;
      linear.getPerpendicular( perp, centre.x, centre.y );
      circle.setCoefficients( diameterSize, diameterSize, centre.x, centre.y );
      circle.getIntersectingPoints( perp, intersects );
      int offset = getMinorPoint( (int)intersects[0], (int)intersects[1], (int)intersects[2], (int)intersects[3] );
      displayBox.setLocation( (int)intersects[0 + offset], (int)intersects[1 + offset] );
      drawStringOffset( minor, displayBox.x, displayBox.y, ignore );
    }

    if ( major != null ) {
      int radiusB = (int) ((30/12) * diameterSize);
      getPointOnLineClosestToB( headPoints[0], headPoints[1], radiusB ); // 30
      linear.getPerpendicular( perp, centre.x, centre.y );
      circle.setCoefficients( radiusB/2, radiusB/2, centre.x, centre.y );
      circle.getIntersectingPoints( perp, intersects );
      int offset = getMajorPoint( (int)intersects[0], (int)intersects[1], (int)intersects[2], (int)intersects[3] );
      displayBox.setLocation( (int)intersects[0 + offset], (int)intersects[1 + offset] );
      drawStringOffset( major, displayBox.x, displayBox.y, false );
    }
  }

  private void drawArrow( int kind ) {
    initArrowHead();
    int noOfPoints = 2;
    xPoints[0] = (int)intersects[0];
    yPoints[0] = (int)intersects[1];
    xPoints[1] = headPoints[0].x;
    yPoints[1] = headPoints[0].y;
    if ( kind != ILineRenderInfo.HALF_OPEN_ARROW_HEAD ) {
      noOfPoints++;
      xPoints[2] = (int)intersects[2];
      yPoints[2] = (int)intersects[3];
    }
    if ( (kind == ILineRenderInfo.WHITE_ARROW_HEAD) || (kind == ILineRenderInfo.BLACK_ARROW_HEAD) ) {
      surface.setColor( (kind == ILineRenderInfo.BLACK_ARROW_HEAD ? line.getColor(): Color.white) );
      surface.fillPolygon( xPoints, yPoints, noOfPoints );
      if ( kind == ILineRenderInfo.WHITE_ARROW_HEAD ) {
        surface.setColor( line.getColor() );
        surface.drawPolygon( xPoints, yPoints, noOfPoints );
      }
    } else
      surface.drawPolyline( xPoints, yPoints, noOfPoints );
  }

  private void drawDiamond( int kind ) {
    initDiamondHead();
    xPoints[0] = headPoints[0].x;
    yPoints[0] = headPoints[0].y;
    xPoints[1] = (int)intersects[0];
    yPoints[1] = (int)intersects[1];
    xPoints[3] = (int)intersects[2];
    yPoints[3] = (int)intersects[3];
    if ( kind == ILineRenderInfo.WHITE_DIAMOND_HEAD ) {
      surface.setColor( Color.white );
      surface.fillPolygon( xPoints, yPoints, 4 );
      surface.setColor( line.getColor() );
      surface.drawPolygon( xPoints, yPoints, 4 );
    } else
      surface.fillPolygon( xPoints, yPoints, 4 );
  }

  private void initArrowHead() {
    int diameterSize = line.getHeadDiameter();

    getPointOnLineClosestToB( headPoints[0], headPoints[1], diameterSize ); // 12
    linear.getPerpendicular( perp, centre.x, centre.y );
    circle.setCoefficients( diameterSize/2, diameterSize/2, centre.x, centre.y );
    circle.getIntersectingPoints( perp, intersects );
    orderHeadPoints();
  }

  private void initDiamondHead() {
    int diameterSize = line.getHeadDiameter();
    getPointOnLineClosestToB( headPoints[0], headPoints[1], diameterSize ); // 12
    xPoints[2] = centre.x;
    yPoints[2] = centre.y;
    getPointOnLineClosestToB( headPoints[0], headPoints[1], diameterSize/2 );  // 6
    linear.getPerpendicular( perp, centre.x, centre.y );
    circle.setCoefficients( diameterSize/2, diameterSize/2, centre.x, centre.y );
    circle.getIntersectingPoints( perp, intersects );
    orderHeadPoints();
  }

  private void calculateCentre() {
    int lineLen = calculateLineLength();
    int halfDist = lineLen / 2;
    Point a = (Point) points.get( 0 );
    a = (Point) points.get( 0 );
    for ( int cntr = 1, dist = 0; cntr < max; cntr++ ) {
      Point b = (Point) points.get( cntr );
      double abDist = a.distance( b );
      dist += abDist;
      if ( dist >= halfDist ) {
        int radius = dist - halfDist;
        radius = (int) abDist - radius;
        getPointOnLineClosestToB( a, b, radius );
        break;
      }
      a = b;
    }
  }

  private void getPointOnLineClosestToB( Point a, Point b, int radius ) {
    linear.setBasedOnPoints( a.x, a.y, b.x, b.y );
    circle.setCoefficients( radius, radius, a.x, a.y );
    circle.getIntersectingPoints( linear, intersects );
    centre.setLocation( intersects[0], intersects[1] );
    double closest = b.distance( centre );
    centre.setLocation( intersects[2], intersects[3] );
    if ( closest < b.distance(centre) )
      centre.setLocation( intersects[0], intersects[1] );
  }

  private int calculateLineLength() {
    int lineLen = 0;
    Point a = (Point) points.get( 0 );
    for ( int cntr = 1; cntr < max; cntr++ ) {
      Point b = (Point) points.get( cntr );
      double dist = a.distance( b );
      lineLen += dist;
      a = b;
    }
    return lineLen;
  }

  private Point getFirstPoint() {
    return (Point) points.get( 0 );
  }

  private Point getSecondPoint() {
    return (Point) points.get( 1 );
  }

  private Point getLastPoint() {
    return (Point) points.get( max-1 );
  }

  private Point getSecondLastPoint() {
    return (Point) points.get( max-2 );
  }

  private void orderHeadPoints() {
    boolean swap = false;
    double x, y;
    if ( intersects[1] > intersects[3] )
      swap = true;

    if ( swap ) {
      x = intersects[0];
      y = intersects[1];
      intersects[0] = intersects[2];
      intersects[1] = intersects[3];
      intersects[2] = x;
      intersects[3] = y;
    }
  }

  private void drawStringOffset( String sMsg, int x, int y, boolean ignore ) {
    if ( (headPoints[0].x > x) && !ignore ) {
      int msgPixelLen = surface.getFontMetrics().stringWidth( sMsg );
      surface.drawString( sMsg, x - msgPixelLen, y );
    } else {
      surface.drawString( sMsg, x, y );
    }
  }

  private int getMajorPoint( int x1, int y1, int x2, int y2 ) {
    if ( x1 == x2 )
      return (y1 < y2 ) ? 0: 2;
    if ( y1 == y2 )
      return ( x1 < x2 ) ? 0: 2;
    if ( y1 < y2 )
      return 0;
    return 2;
  }

  private int getMinorPoint( int x1, int y1, int x2, int y2 ) {
    if ( x1 == x2 )
      return (y1 > y2 ) ? 0: 2;
    if ( y1 == y2 )
      return ( x1 > x2 ) ? 0: 2;
    if ( y1 > y2 )
      return 0;
    return 2;
  }
}